/*
 * Copyright (c) 2006-2021, RT-Thread Development Team
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author       Notes
 * 2017-02-27     Bernard      fix the re-work issue.
 * 2021-08-01     Meco Man     remove cc_delayed_work_init()
 * 2021-08-14     Jackistang   add comments for function interface.
 */

#include "cc_workqueue.h"
#include "cc_log.h"

static void _delayed_work_timeout_handler(void* parameter);

cc_inline int _workqueue_work_completion(struct cc_workqueue* queue)
{
  int result;
  // uint32_t value = cc_thread_enter_critical();
  while (1)
  {
    /* try to take condition semaphore */
    result = cc_sem_try_wait(&(queue->sem));
    if (result == -CC_ETIMEDOUT)
    {
      /* it's timeout, release this semaphore */
      cc_sem_release(&(queue->sem));
    }
    else if (result == CC_EOK)
    {
      /* keep the sem value = 0 */
      result = CC_EOK;
      break;
    }
    else
    {
      result = -CC_ERROR;
      break;
    }
  }
  // cc_thread_exit_critical(value);

  return result;
}

static void _workqueue_thread_entry(void* parameter)
{
  uint32_t level;
  struct cc_work* work;
  struct cc_workqueue* queue;

  queue = (struct cc_workqueue*)parameter;

  while (1)
  {
    // level = cc_thread_enter_critical();
    if (cc_list_isempty(&(queue->work_list)))
    {
      /* no software timer exist, suspend self. */
      cc_thread_suspend(cc_thread_self());
      // cc_thread_exit_critical(level);
      cc_thread_yield();
      continue;
    }

    /* we have work to do with. */
    void* data;
    cc_list_get_entry(&queue->work_list, data);
    work = (struct cc_work*)data;
    cc_list_remove(&(work->list));
    queue->work_current = work;
    work->flags &= ~CC_WORK_STATE_PENDING;
    work->workqueue = CC_NULL;
    // cc_thread_exit_critical(level);

    /* do work */
    work->work_func(work, work->work_data);
    /* clean current work */
    queue->work_current = CC_NULL;

    /* ack work completion */
    _workqueue_work_completion(queue);
  }
}

static int _workqueue_submit_work(struct cc_workqueue* queue,
                                  struct cc_work* work, uint32_t ticks)
{
  uint32_t level;
  int err;
  if (queue == CC_NULL || work == CC_NULL)
  {
    cc_log_e("no mem\r\n");
    return CC_ENOMEM;
  }
  // level = cc_thread_enter_critical();
  /* remove list */
  // cc_list_remove(&(work->list));
  work->flags &= ~CC_WORK_STATE_PENDING;
  /*  */
  if (ticks == 0)
  {
    if (queue->work_current != work)
    {
      cc_list_node_set_entry(&(work->list), work);
      cc_list_insert_after(&queue->work_list, &(work->list));
      work->flags |= CC_WORK_STATE_PENDING;
      work->workqueue = queue;
      err = CC_EOK;
    }
    else
    {
      err = -CC_EBUSY;
    }

    /* whether the workqueue is doing work */
    if (queue->work_current == CC_NULL && cc_thread_get_state(queue->work_thread) == eSuspended)
    {
      /* resume work thread */
      cc_thread_resume(queue->work_thread);
      // cc_thread_exit_critical(level);
      cc_thread_yield();
    }
    else
    {
      // cc_thread_exit_critical(level);
    }
    return err;
  }
  else if (ticks < 0xffffffff / 2)
  {
    /* Timer started */
    if (work->flags & CC_WORK_STATE_SUBMITTING)
    {
      cc_timer_change_time_and_stop(&work->timer, ticks);
    }
    else
    {
      cc_timer_init(&(work->timer), _delayed_work_timeout_handler, cc_timer_one_shot,
                    ticks, work);
      work->flags |= CC_WORK_STATE_SUBMITTING;
    }
    work->workqueue = queue;
    /* insert delay work list */
    cc_list_node_set_entry(&(work->list), work);
    cc_list_insert_after(&queue->delayed_list, &(work->list));
    // cc_thread_exit_critical(level);
    cc_timer_start(&(work->timer));
    return CC_EOK;
  }
  // cc_thread_exit_critical(level);
  return -CC_ERROR;
}

static int _workqueue_cancel_work(struct cc_workqueue* queue, struct cc_work* work)
{
  uint32_t level;
  int err;
  if (queue == CC_NULL || work == CC_NULL)
  {
    cc_log_e("no mem\r\n");
    return CC_ENOMEM;
  }

  // level = cc_thread_enter_critical();
  cc_list_remove(&(work->list));
  work->flags &= ~CC_WORK_STATE_PENDING;
  /* Timer started */
  if (work->flags & CC_WORK_STATE_SUBMITTING)
  {
    cc_timer_stop(&(work->timer));
    cc_timer_deinit(&(work->timer));
    work->flags &= ~CC_WORK_STATE_SUBMITTING;
  }
  err = queue->work_current != work ? CC_EOK : -CC_EBUSY;
  work->workqueue = CC_NULL;
  // cc_thread_exit_critical(level);
  return err;
}

static void _delayed_work_timeout_handler(void* parameter)
{
  struct cc_work* work = (struct cc_work*)cc_timer_get_data(parameter);

  struct cc_workqueue* queue = work->workqueue;
  uint32_t level;

  // level = cc_thread_enter_critical();
  cc_timer_deinit(&(work->timer));
  work->flags &= ~CC_WORK_STATE_SUBMITTING;
  /* remove delay list */
  cc_list_remove(&(work->list));
  /* insert work queue */
  if (queue->work_current != work)
  {
    cc_list_node_set_entry(&(work->list), work);
    cc_list_insert_after(&queue->work_list, &(work->list));
    work->flags |= CC_WORK_STATE_PENDING;
  }
  /* whether the workqueue is doing work */
  if (queue->work_current == CC_NULL && cc_thread_get_state(queue->work_thread) == eSuspended)
  {
    /* resume work thread */
    cc_thread_resume(queue->work_thread);
    // cc_thread_exit_critical(level);
    cc_thread_yield();
  }
  else
  {
    // cc_thread_exit_critical(level);
  }
}

/**
 * @brief Create a work queue with a thread inside.
 *
 * @param name          The name of the work queue thread.
 * @param stack_size    The stack size of the work queue thread.
 * @param priority      The priority of the work queue thread.
 *
 * @return Return A pointer to the workqueue object. It will return CC_NULL if failed.
 */
struct cc_workqueue* cc_workqueue_create(const char* name, uint16_t stack_size, uint8_t priority)
{
  struct cc_workqueue* queue = CC_NULL;

  queue = (struct cc_workqueue*)cc_malloc(sizeof(struct cc_workqueue));
  if (queue == CC_NULL)
  {
    cc_log_e("no mem\r\n");
    return CC_NULL;
  }

  /* initialize work list */
  cc_list_init(&(queue->work_list));
  cc_list_init(&(queue->delayed_list));
  queue->work_current = CC_NULL;
  cc_sem_init(&(queue->sem), 0, 32);

  /* create the work thread */
  queue->work_thread = cc_thread_create_raw(name, stack_size, priority, _workqueue_thread_entry, queue);
  if (queue->work_thread == CC_NULL)
  {
    cc_free(queue);
    return CC_NULL;
  }

  return queue;
}

/**
 * @brief Destroy a work queue.
 *
 * @param queue         A pointer to the workqueue object.
 *
 * @return CC_EOK       Success.
 */
int cc_workqueue_destroy(struct cc_workqueue* queue)
{

  cc_workqueue_cancel_all_work(queue);
  cc_thread_delete(queue->work_thread);
  cc_sem_deinit(&(queue->sem));
  cc_free(queue);

  return CC_EOK;
}

/**
 * @brief Submit a work item to the work queue without delay.
 *
 * @param queue         A pointer to the workqueue object.
 * @param work          A pointer to the work item object.
 *
 * @return CC_EOK       Success.
 * @return -CC_EBUSY    This work item is executing.
 */
int cc_workqueue_dowork(struct cc_workqueue* queue, struct cc_work* work)
{

  if (queue != CC_NULL || work == CC_NULL)
  {
    cc_log_e("no mem\r\n");
    return CC_ENOMEM;
  }
  return _workqueue_submit_work(queue, work, 0);
}

/**
 * @brief Submit a work item to the work queue with a delay.
 *
 * @param queue     A pointer to the workqueue object.
 * @param work      A pointer to the work item object.
 * @param time      The delay time (unit: OS ticks) for the work item to be submitted to the work queue.
 *
 * @return CC_EOK       Success.
 * @return -CC_EBUSY    This work item is executing.
 * @return -CC_ERROR    The time parameter is invalid.
 */
int cc_workqueue_submit_work(struct cc_workqueue* queue, struct cc_work* work, uint32_t time)
{

  return _workqueue_submit_work(queue, work, time);
}

/**
 * @brief Submit a work item to the work queue without delay. This work item will be executed after the current work item.
 *
 * @param queue     A pointer to the workqueue object.
 * @param work      A pointer to the work item object.
 *
 * @return CC_EOK   Success.
 */
int cc_workqueue_critical_work(struct cc_workqueue* queue, struct cc_work* work)
{
  uint32_t level;
  if (queue == CC_NULL || work == CC_NULL)
  {
    cc_log_e("no mem\r\n");
    return CC_ENOMEM;
  }
  // level = cc_thread_enter_critical();
  /* NOTE: the work MUST be initialized firstly */
  cc_list_remove(&(work->list));
  cc_list_node_set_entry(&(work->list), work);

  cc_list_insert_after(&queue->work_list, &(work->list));
  /* whether the workqueue is doing work */
  if (queue->work_current == CC_NULL && cc_thread_get_state(queue->work_thread) == eSuspended)
  {
    /* resume work thread */
    cc_thread_resume(queue->work_thread);
    // cc_thread_exit_critical(level);
    cc_thread_yield();
  }
  else
  {
    // cc_thread_exit_critical(level);
  }

  return CC_EOK;
}

/**
 * @brief Cancel a work item in the work queue.
 *
 * @param queue     A pointer to the workqueue object.
 * @param work      A pointer to the work item object.
 *
 * @return CC_EOK       Success.
 * @return -CC_EBUSY    This work item is executing.
 */
int cc_workqueue_cancel_work(struct cc_workqueue* queue, struct cc_work* work)
{

  return _workqueue_cancel_work(queue, work);
}

/**
 * @brief Cancel a work item in the work queue. If the work item is executing, this function will block until it is done.
 *
 * @param queue     A pointer to the workqueue object.
 * @param work      A pointer to the work item object.
 *
 * @return CC_EOK       Success.
 */
int cc_workqueue_cancel_work_sync(struct cc_workqueue* queue, struct cc_work* work)
{

  if (queue->work_current == work) /* it's current work in the queue */
  {
    /* wait for work completion */
    cc_sem_wait(&(queue->sem));
  }
  else
  {
    return _workqueue_cancel_work(queue, work);
  }

  return CC_EOK;
}

/**
 * @brief This function will cancel all work items in work queue.
 *
 * @param queue     A pointer to the workqueue object.
 *
 * @return CC_EOK       Success.
 */
int cc_workqueue_cancel_all_work(struct cc_workqueue* queue)
{
  struct cc_work* work;

  /* cancel work */
  // cc_thread_enter_critical();
  while (cc_list_isempty(&queue->work_list) == 0)
  {
    void* data;
    cc_list_get_entry(&queue->work_list, data);
    work = (cc_work*)data;
    _workqueue_cancel_work(queue, work);
  }
  /* cancel delay work */
  while (cc_list_isempty(&queue->delayed_list) == 0)
  {
    void* data;
    cc_list_get_entry(&queue->delayed_list, data);
    work = (cc_work*)data;
    _workqueue_cancel_work(queue, work);
  }
  // cc_thread_exit_critical();

  return CC_EOK;
}

static struct cc_workqueue* sys_workq;

/**
 * @brief Submit a work item to the system work queue with a delay.
 *
 * @param work      A pointer to the work item object.
 * @param time      The delay time (unit: OS ticks) for the work item to be submitted to the work queue.
 *
 * @return CC_EOK       Success.
 * @return -CC_EBUSY    This work item is executing.
 * @return -CC_ERROR    The time parameter is invalid.
 */
int cc_work_submit(struct cc_work* work, uint32_t time)
{
  return cc_workqueue_submit_work(sys_workq, work, time);
}

/**
 * @brief Cancel a work item in the system work queue.
 *
 * @param work      A pointer to the work item object.
 *
 * @return CC_EOK       Success.
 * @return -CC_EBUSY    This work item is executing.
 */
int cc_work_cancel(struct cc_work* work)
{
  return cc_workqueue_cancel_work(sys_workq, work);
}

static int cc_work_sys_workqueue_init(void)
{
  if (sys_workq != CC_NULL)
    return CC_EOK;

  sys_workq = cc_workqueue_create("sys_work", 2048,
                                  8);
  if (sys_workq == CC_NULL)
  {
    cc_log_e("no mem\r\n");
    return CC_ENOMEM;
  }
  return cc_true;
}
CC_INIT_ENV_REGISTER(cc_work_sys_workqueue_init);

int cc_work_init(struct cc_work* work, void (*work_func)(struct cc_work* work, void* work_data),
                 void* work_data)
{
  if (work == CC_NULL || work_func == CC_NULL)
  {
    cc_log_e("no mem\r\n");
    return CC_ENOMEM;
  }
  cc_list_node_init(&(work->list));
  work->work_func = work_func;
  work->work_data = work_data;
  work->workqueue = CC_NULL;
  work->flags = 0;
  work->type = 0;
  return CC_EOK;
}